/*
 * IdentityComonentDefinition.java
 *
 * Created on September 2, 2006, 4:37 PM
 *
 * (C) 2006 R. Alexander Milowski
 */

package org.infoset.component.library;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.infoset.component.Component;
import org.infoset.component.ComponentBase;
import org.infoset.component.ComponentDefinitionBase;
import org.infoset.xml.Attribute;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.infoset.xml.ElementEnd;
import org.infoset.xml.Item;
import org.infoset.xml.ItemDestination;
import org.infoset.xml.Name;
import org.infoset.xml.Validity;
import org.infoset.xml.XMLException;
import org.infoset.xml.filter.ItemFilter;
import org.infoset.xml.util.DocumentDestination;
import org.infoset.xml.util.DocumentSource;
import org.infoset.xml.util.XMLWriter;
import org.infoset.xml.xerces.XMLSchemaValidationFilter;

/**
 *
 * @author alex
 */
public class XMLSchemaComponentDefinition extends ComponentDefinitionBase {
   
   static Logger LOG = Logger.getLogger(XSLTComponentDefinition.class.getName());
   static Name DOCUMENT_INPUT_PORT = Name.create("source");
   static Name SCHEMA_INPUT_PORT = Name.create("schema");
   static Name RESULT_OUTPUT_PORT = Name.create("result");
   
   static Name SCHEMA_MAP = Name.create("schema-map");
   static Name SCHEMA = Name.create("{http://www.w3.org/2001/XMLSchema}schema");
   static Name MAP = Name.create("map");
   
   static List<Name> inputs = new ArrayList<Name>();
   static List<Name> outputs = new ArrayList<Name>();
   static {
      inputs.add(DOCUMENT_INPUT_PORT);
      inputs.add(SCHEMA_INPUT_PORT);
      outputs.add(RESULT_OUTPUT_PORT);
   }

   public Name getDOCUMENT_INPUT_PORT() {
      return DOCUMENT_INPUT_PORT;
   }
   
   public class XMLSchemaComponent extends ComponentBase {
      
      XMLSchemaValidationFilter filter;
      ItemFilter end;
      Document inputDocument;
      boolean hasSchemata;
      ItemDestination output;
      boolean done;
      boolean assertValid;
      
      public XMLSchemaComponent(Name name,String vendor,String version) {
         super(name,vendor,version);
         this.filter = null;
         this.inputDocument = null;
         this.output = null;
         this.end = null;
         this.hasSchemata = false;
         this.assertValid = false;
      }
      
      public void init(Component.Context context) 
         throws XMLException
      {
         super.init(context);
         this.filter = new XMLSchemaValidationFilter();
         Object o = context.getParameter(Name.create("assert-valid"));
         this.assertValid = o==null ? true : "true".equals(o.toString());
         if (assertValid) {
            this.end = new ItemFilter() {
               int level = -1;
               ItemDestination outputDest = null;
               public void attach(ItemDestination out) {
                  outputDest = out;
               }
               public void send(Item item)
                  throws XMLException
               {
                  switch (item.getType()) {
                     case DocumentItem:
                        level = -1;
                        break;
                     case ElementItem:
                        level++;
                        break;
                     case ElementEndItem:
                        level--;
                        ElementEnd end = (ElementEnd)item;
                        if (LOG.isLoggable(Level.FINER)) {
                           LOG.info(end.getName()+" Validation attempted: "+end.getValidationAttempted()+" (full="+Validity.ATTEMPTED_FULL+", partial="+Validity.ATTEMPTED_PARTIAL+", none="+Validity.ATTEMPTED_NONE+"), Validity: "+end.getValidity()+" (invalid="+Validity.INVALID+",not known="+Validity.NOT_KNOWN+",valid="+Validity.VALID+")");
                        }
                        if (level<0 && end.getValidity()!=Validity.VALID) {
                           Iterator errors = filter.getErrors();
                           while (errors.hasNext()) {
                              LOG.severe(errors.next().toString());
                           }
                           throw new XMLException("Element "+end.getName()+" is not valid.");
                        }
                        break;
                  }
                  if (outputDest!=null) {
                     outputDest.send(item);
                  }
               }
            };
            filter.attach(end);
         } else {
            this.end = filter;
         }
         
         this.hasSchemata = false;
      }
      
      void loadSchemaMap(Document document) 
         throws IOException
      {
         Iterator<Element> children = document.getDocumentElement().getElementsByName(MAP);
         while (children.hasNext()) {
            Element child = children.next();
            String namespaceValue = child.getAttributeValue("namespace");
            URI namespace = namespaceValue==null ? Name.NO_NAMESPACE : URI.create(namespaceValue);
            String href = child.getAttributeValue("href");
            URI location = child.getBaseURI().resolve(href);
            filter.addNamespaceMap(namespace,location.toURL());
         }
      }
      
      public void bindResource(Name port,Document document)
         throws XMLException 
      {
         if (port.equals(DOCUMENT_INPUT_PORT)) {
            // Save the input document
            inputDocument = document;
         } else if (port.equals(SCHEMA_INPUT_PORT)) {
            if (document.getDocumentElement().getName().equals(SCHEMA_MAP)) {
               try {
                  loadSchemaMap(document);
                  hasSchemata = true;
               } catch (IOException ex) {
                  throw new XMLException("Cannot resolve URLs in map: "+ex.getMessage(),ex);
               }
            } else {
               bindSchemaDocument(document);
            }
         } else {
            throw new XMLException("Binding port "+port+" via ItemSource is not supported.");
         }
      }
      
      protected void bindSchemaDocument(Document document)
         throws XMLException
      {
         try {
            Element top = document.getDocumentElement();
            if (!top.getName().equals(SCHEMA)) {
               throw new XMLException("Unrecognized document element "+document.getDocumentElement().getName()+" for schema port.");
            }
            String uriString = top.getAttributeValue("targetNamespace");
            if (document.getBaseURI()!=null) {
               if (uriString==null) {
                  filter.addNamespaceMap(Name.NO_NAMESPACE,document.getBaseURI().toURL());
               } else {
                  filter.addNamespaceMap(URI.create(uriString),document.getBaseURI().toURL());
               }
            } else {
               File tempLoc = File.createTempFile("schema","xsd");
               document.getDocumentElement().setAttributeValue(Attribute.XML_BASE,document.getBaseURI().toString());
               XMLWriter.writeDocument(document,new FileWriter(tempLoc));
               if (uriString==null) {
                  filter.addNamespaceMap(Name.NO_NAMESPACE,tempLoc.toURL());
               } else {
                  filter.addNamespaceMap(URI.create(uriString),tempLoc.toURL());
               }
            }
         } catch (IOException ex) {
            throw new XMLException("Cannot resolve URLs in map: "+ex.getMessage(),ex);
         }
         
      }
   
      public void bindResource(Name port,URI location)
         throws XMLException
      {
         if (port.equals(DOCUMENT_INPUT_PORT)) {
            try {
               inputDocument = context.getDocumentLoader().load(location);
            } catch (IOException ex) {
               throw new XMLException("Cannot load input document due to I/O error.",ex);
            }
         } else if (port.equals(SCHEMA_INPUT_PORT)) {
            try {
               Document document = context.getDocumentLoader().load(location);
               if (document.getDocumentElement().getName().equals(SCHEMA_MAP)) {
                  loadSchemaMap(document);
               } else {
                  String uriString = document.getDocumentElement().getAttributeValue("targetNamespace");
                  if (uriString==null) {
                     filter.addNamespaceMap(Name.NO_NAMESPACE,location.toURL());
                  } else {
                     filter.addNamespaceMap(URI.create(uriString),location.toURL());
                  }
               }
               hasSchemata = true;
            } catch (IOException ex) {
               throw new XMLException("Cannot load input document due to I/O error.",ex);
            }
         } else {
            super.bindResource(port,location);
         }
      }
      public void bindOutputPort(Name port,ItemDestination output)
         throws XMLException
      {
         this.output = output;
         if (port.equals(RESULT_OUTPUT_PORT)) {
            end.attach(output);
         } else {
            throw new XMLException("Unknown output port "+port+" on "+name);
         }
      }

      
      public void onStart() 
         throws XMLException
      {
         done = false;
         if (inputDocument!=null) {
            // Perform the transform because we have all the inputs
            DocumentSource.generate(inputDocument,false,filter);
            done = true;
         }
      }
      
      public ItemDestination getInputPort(Name port) 
         throws XMLException
      {
         if (port.equals(DOCUMENT_INPUT_PORT)) {
            if (hasSchemata) {
               return filter;
            } else {
               return new DocumentDestination() {
                  public void send(Item item) 
                     throws XMLException
                  {
                     super.send(item);
                     if (item.getType()==Item.ItemType.DocumentEndItem) {
                        inputDocument = getDocument();
                        if (hasSchemata) {
                           DocumentSource.generate(inputDocument,false,filter);
                           done = true;
                        }
                     }
                  }
               };
            }
         } else if (port.equals(SCHEMA_INPUT_PORT)) {
            return new DocumentDestination() {
               public void send(Item item) 
                  throws XMLException
               {
                  super.send(item);
                  if (item.getType()==Item.ItemType.DocumentEndItem) {
                     Document document = getDocument();
                     if (document.getDocumentElement().getName().equals(SCHEMA_MAP)) {
                        try {
                           loadSchemaMap(document);
                           hasSchemata = true;
                           if (inputDocument!=null) {
                              DocumentSource.generate(inputDocument,false,filter);
                              done = true;
                           }
                        } catch (IOException ex) {
                           throw new XMLException("Cannot resolve URLs in map: "+ex.getMessage(),ex);
                        }
                     } else {
                        bindSchemaDocument(document);
                     }
                  }
               }
            };
         } else {
            super.getInputPort(port);
         }
         return null;
      }
      
      public void onDocumentInput(Name port,Document document) 
         throws XMLException
      {
         if (port.equals(DOCUMENT_INPUT_PORT)) {
            if (inputDocument!=null) {
               throw new XMLException("The input document has already be receieved.");
            }
            inputDocument = document;
            
            if (hasSchemata) {
               // Perform the transform as we now have the input document
               DocumentSource.generate(inputDocument,false,filter);
               done = true;
            }
         } else if (port.equals(SCHEMA_INPUT_PORT)) {
            if (document.getDocumentElement().getName().equals(SCHEMA_MAP)) {
               try {
                  loadSchemaMap(document);
                  hasSchemata = true;
               } catch (IOException ex) {
                  throw new XMLException("Cannot resolve URLs in map: "+ex.getMessage(),ex);
               }
            } else {
               bindSchemaDocument(document);
            }
            if (inputDocument!=null) {
               // Perform the transform as we now have the transform
               DocumentSource.generate(inputDocument,false,filter);
               done = true;
            }
         } else {
            super.getInputPort(port);
         }
      }
      
      public void onEnd()
         throws XMLException
      {
         if (!done) {
            onStart();
         }
         Iterator errors = filter.getErrors();
         while (errors.hasNext()) {
            LOG.severe(errors.next().toString());
         }
         inputDocument = null;
      }
   }

   /** Creates a new instance of IdentityComonentDefinition */
   public XMLSchemaComponentDefinition(Name name) {
      super(name,null,null,null);
   }
   
   /** Creates a new instance of IdentityComonentDefinition */
   public XMLSchemaComponentDefinition(Name name,String vendor,String version,URI vendorLocation) {
      super(name,vendor,version,vendorLocation);
   }
   
   public Component newInstance() 
      throws XMLException
   {
      return new XMLSchemaComponent(getName(),getVendor(),getVersion());
   }
   
   public Collection<Name> getInputs() {
      return inputs;
   }
   
   public Collection<Name> getOutputs() {
      return outputs;
   }

}
